// Script Manager
// (c) jimon game studio

#include "CScriptManager_AngelScript.h"
#include "ILogManager.h"
#include "ITimer.h"
#include "IVariables.h"
#include "jeMemoryBuffer.h"

#include "jeScriptString.h"
#include "AngelScript/scriptbuilder.h"
#include "AngelScript/scriptmath.h"
#include "AngelScript/scriptmath3d.h"

namespace je
{
	namespace core
	{
		IMPLEMENT_SINGLETON_N(ScriptManager,CScriptManager_AngelScript,JE_ENGINE_FUNCTION_CALL)

		void AngelScript_MessageCallback(const asSMessageInfo * msg, void * param)
		{
			const char * type = "ERR ";

			if( msg->type == asMSGTYPE_WARNING )
				type = "WARN";
			else if( msg->type == asMSGTYPE_INFORMATION )
				type = "INFO";

			c8 temp[1024];
			#ifdef JE_MSVC
			sprintf_s(temp,1024,"ScriptLog : %s (%d, %d) : %s : %s", msg->section, msg->row, msg->col, type, msg->message);
			#else
			sprintf(temp,"ScriptLog : %s (%d, %d) : %s : %s", msg->section, msg->row, msg->col, type, msg->message);
			#endif

			JELOG(temp)
		}

		void AngelScriptContextLineCallback(asIScriptContext * ScriptContext, f64 * timeOut)
		{
			if( *timeOut < GetTimer()->GetTime() )
				ScriptContext->Abort();
		}

		void Cap_Grab(void*)
		{
		}

		void Cap_Drop(void*)
		{
		}

		class AngelSciprtBufferWrapper:public asIBinaryStream
		{
		protected:
			IBuffer * Buffer;
			jeMemoryBuffer * TempBuffer;
		public:
			AngelSciprtBufferWrapper(IBuffer * buffer)
				:Buffer(buffer),TempBuffer(NULL)
			{
				Buffer->Grab();
			}

			~AngelSciprtBufferWrapper()
			{
				if(TempBuffer)
				{
					if(TempBuffer->IsLocked())
						TempBuffer->UnLock();
					TempBuffer->Drop();
				}

				if(Buffer->IsLocked())
					Buffer->UnLock();

				Buffer->Drop();
			}

			void WriteToBuffer()
			{
				Buffer->Lock(BLT_WRITE);
				Buffer->Allocate(TempBuffer->GetSize());
				Buffer->SetPosition(0);
				Buffer->Write(TempBuffer->GetPtr(),TempBuffer->GetSize());
				Buffer->UnLock();
			}

			void Read(void *ptr, asUINT size)
			{
				if(!Buffer->IsLocked())
				{
					Buffer->Lock(core::BLT_READ);
					Buffer->SetPosition(0);
					Buffer->SetStreamWorkMode(true);
				}

				Buffer->Read(ptr,size);
			}

			void Write(const void *ptr, asUINT size)
			{
				if(!TempBuffer)
				{
					JENEW(TempBuffer,jeMemoryBuffer)
					TempBuffer->Lock(core::BLT_WRITE);
					TempBuffer->SetPosition(0);
					TempBuffer->SetStreamWorkMode(true);
				}

				TempBuffer->Allocate(size + TempBuffer->GetSize());
				TempBuffer->Write(ptr,size);
			}
		};

		//! Constructor
		CScriptManager_AngelScript::CScriptManager_AngelScript()
		{
			asSetGlobalMemoryFunctions(Alloc,Free);

			Engine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
			JEASSERT(Engine)

			RegisterJeScriptString(Engine);
			RegisterScriptMath(Engine);
			RegisterScriptMath3D(Engine);

			Engine->SetMessageCallback(asFUNCTION(AngelScript_MessageCallback),0,asCALL_CDECL);

			ScriptContext = Engine->CreateContext();

			timeOut = 0;
			ScriptContext->SetLineCallback(asFUNCTION(AngelScriptContextLineCallback), &timeOut, asCALL_CDECL);

			SetScriptManager(this);
		}

		//! Destructor
		CScriptManager_AngelScript::~CScriptManager_AngelScript()
		{
			if(ScriptContext)
				ScriptContext->Release();

			for(ClassesMapType::Iterator Iter=Registration.getIterator();!Iter.atEnd();Iter++)
				JEDELETE(Iter->getValue())
			Registration.clear();
			Engine->Release();
			SetScriptManager(NULL);
		}

		//! Load Script
		u1 CScriptManager_AngelScript::LoadScript(core::IBuffer * Buffer)
		{
			c8 * Temp = NULL;
			JENEW_A(Temp,c8,Buffer->GetSize()+1)
			Buffer->Lock(core::BLT_READ);
			Buffer->SetPosition(0);
			Buffer->Read(Temp,Buffer->GetSize());
			Buffer->UnLock();
			Temp[Buffer->GetSize()] = 0;

			CScriptBuilder Builder;
			s32 r = -1;

			Builder.StartNewModule(Engine,"");
			Builder.AddSectionFromMemory(Temp,"");
			r = Builder.BuildModule();

			JEDELETE_A(Temp)

			if(r < 0)
			{
				JELOG_E("Build script failed")
				return false;
			}
			else
				return true;
		}

		//! Load Byte Code
		u1 CScriptManager_AngelScript::LoadByteCode(core::IBuffer * Buffer)
		{
			AngelSciprtBufferWrapper * BufferWrapper = NULL;
			JENEW(BufferWrapper,AngelSciprtBufferWrapper(Buffer));
			s32 r = Engine->GetModule(0,asGM_ALWAYS_CREATE)->LoadByteCode(BufferWrapper);
			JEDELETE(BufferWrapper)

			if(r < 0)
			{
				JELOG_E("Load byte code failed")
				return false;
			}
			else
				return true;
		}

		//! Save Byte Code
		core::IBuffer * CScriptManager_AngelScript::SaveByteCode()
		{
			jeMemoryBuffer * Buffer = NULL;
			JENEW(Buffer,jeMemoryBuffer)
			AngelSciprtBufferWrapper * BufferWrapper = NULL;
			JENEW(BufferWrapper,AngelSciprtBufferWrapper(Buffer));
			Engine->GetModule(0)->SaveByteCode(BufferWrapper);
			BufferWrapper->WriteToBuffer();
			JEDELETE(BufferWrapper)
			return Buffer;
		}

		//! Register Function
		void CScriptManager_AngelScript::RegisterFunction(const jeStringc & Declaration,const jeScriptFunction & FunctionPtr)
		{
			Engine->RegisterGlobalFunction(Declaration.c_str(),asFUNCTION(FunctionPtr),asCALL_CDECL);
		}

		//! Register Class
		void CScriptManager_AngelScript::RegisterClass(const jeStringc & ClassName,s32 ClassSize)
		{
			Engine->RegisterObjectType(ClassName.c_str(),ClassSize,asOBJ_REF);
			Engine->RegisterObjectBehaviour(ClassName.c_str(),asBEHAVE_ADDREF,"void f()",asFUNCTION(Cap_Grab),asCALL_CDECL_OBJFIRST);
			Engine->RegisterObjectBehaviour(ClassName.c_str(),asBEHAVE_RELEASE,"void f()",asFUNCTION(Cap_Drop),asCALL_CDECL_OBJFIRST);

			ClassRegEntry * Entry = NULL;
			JENEW(Entry,ClassRegEntry)
			Entry->Name = ClassName;
			Entry->Size = ClassSize;
			Registration.set(ClassName,Entry);
		}

		//! Register Class
		void CScriptManager_AngelScript::RegisterClassBase(const jeStringc & ClassName,const jeStringc & BaseClassName,s32 ClassSize)
		{
			Engine->RegisterObjectType(ClassName.c_str(),ClassSize,asOBJ_REF);
			Engine->RegisterObjectBehaviour(ClassName.c_str(),asBEHAVE_ADDREF,"void f()",asFUNCTION(Cap_Grab),asCALL_CDECL_OBJFIRST);
			Engine->RegisterObjectBehaviour(ClassName.c_str(),asBEHAVE_RELEASE,"void f()",asFUNCTION(Cap_Drop),asCALL_CDECL_OBJFIRST);

			ClassesMapType::Node * Node = Registration.find(BaseClassName);
			if(Node)
			{
				ClassRegEntry * Entry = NULL;
				JENEW(Entry,ClassRegEntry)
				Entry->Name = ClassName;
				Entry->Size = ClassSize;
				Entry->BaseClass = Node->getValue();
				Registration.set(ClassName,Entry);

				Node->getValue()->RegisterMethodsAndVars(ClassName,Engine);
			}
			else
				JELOG_E(jeStringc("Cant find base class '")+BaseClassName+jeStringc("'"))
		}

		//! Register Class Method
		void CScriptManager_AngelScript::RegisterClassMethod(const jeStringc & ClassName,const jeStringc & MethodDefinition,jeScriptMethodPtrBase * MethodPtr)
		{
			asSFuncPtr p;
			JEASSERT(sizeof(p) >= (u32)MethodPtr->GetPtrSize())
			asMemClear(&p,sizeof(p));
			asMemCopy(&p,MethodPtr->GetPtrData(),MethodPtr->GetPtrSize());
			p.flag = 3;

			Engine->RegisterObjectMethod(ClassName.c_str(),MethodDefinition.c_str(),p,asCALL_THISCALL);

			ClassesMapType::Node * Node = Registration.find(ClassName);
			if(Node)
			{
				ClassRegEntry::RegMethod * Meth = NULL;
				JENEW(Meth,ClassRegEntry::RegMethod(MethodDefinition,MethodPtr))
				Node->getValue()->Methods.push_back(Meth);
			}
			else
				JELOG_W("Cant find registration class entry")
		}

		//! Register Class Method Variable
		void CScriptManager_AngelScript::RegisterClassVariable(const jeStringc & ClassName,const jeStringc & VariableDefinition,s32 VariableOffset)
		{
			Engine->RegisterObjectProperty(ClassName.c_str(),VariableDefinition.c_str(),VariableOffset);

			ClassesMapType::Node * Node = Registration.find(ClassName);
			if(Node)
			{
				ClassRegEntry::RegVar * Var = NULL;
				JENEW(Var,ClassRegEntry::RegVar(VariableDefinition,VariableOffset))
				Node->getValue()->Vars.push_back(Var);
			}
			else
				JELOG_W("Cant find registration class entry")
		}

		//! Register Global Variable
		void CScriptManager_AngelScript::RegisterGlobalVariable(const jeStringc & Declaration,void * Pointer)
		{
			Engine->RegisterGlobalProperty(Declaration.c_str(),Pointer);
		}

		//! Execute Command
		void CScriptManager_AngelScript::ExecuteCommand(const jeStringc & Command)
		{
			Engine->ExecuteString(0,Command.c_str());
		}

		//! Gargabe Collector
		void CScriptManager_AngelScript::GarbageCollector()
		{
			Engine->GarbageCollect(asGC_ONE_STEP);
		}

		//! Add Function Param
		void CScriptManager_AngelScript::AddParam(u1 Param)
		{
			ScriptContext->SetArgByte(ArgNum,Param);
			ArgNum++;
		}

		//! Add Function Param
		void CScriptManager_AngelScript::AddParam(c8 Param)
		{
			ScriptContext->SetArgByte(ArgNum,Param);
			ArgNum++;
		}

		//! Add Function Param
		void CScriptManager_AngelScript::AddParam(s32 Param)
		{
			ScriptContext->SetArgDWord(ArgNum,Param);
			ArgNum++;
		}

		//! Add Function Param
		void CScriptManager_AngelScript::AddParam(f32 Param)
		{
			ScriptContext->SetArgFloat(ArgNum,Param);
			ArgNum++;
		}

		//! Add Function Param
		void CScriptManager_AngelScript::AddParam(void * Param)
		{
			ScriptContext->SetArgAddress(ArgNum,Param);
			ArgNum++;
		}

		//! Call Internal
		u1 CScriptManager_AngelScript::CallInternal(s32 CallId,void * ObjectPtr)
		{
			s32 Result = 0;
			Result = ScriptContext->Prepare(CallId);
			if(Result)
			{
				JELOG_E("Can't prepare script context")
				return false;
			}

			if(ObjectPtr)
			{
				Result = ScriptContext->SetObject(ObjectPtr);
				if(Result)
				{
					JELOG_E("Can't set script object")
					return false;
				}
			}

			timeOut = core::GetTimer()->GetTime() + core::GetVariables()->GetVariablef32("AngelScript_TimeOut");
			ArgNum = 0;

			return true;
		}


		//! Call Function,return true if successful
		u1 CScriptManager_AngelScript::CallFunction(const jeStringc & FunctionName)
		{
			s32 funcId = Engine->GetModule(0)->GetFunctionIdByDecl(FunctionName.c_str());
			if(funcId < 0)
			{
				JELOG_E(jeStringc("Can't find function : ")+FunctionName)
				return false;
			}

			return CallInternal(funcId);
		}

		//! Call Method,return true if successful
		u1 CScriptManager_AngelScript::CallMethod(const jeStringc & MethodName,IScriptObject * Object)
		{
			CScriptObject_AngelScript * AngelObject = static_cast<CScriptObject_AngelScript*>(Object);

			asIObjectType * ObjType = Engine->GetObjectTypeById(AngelObject->TypeId);
			if(!ObjType)
			{
				JELOG_E(jeStringc("Can't find object type : ")+AngelObject->TypeId)
				return false;
			}

			s32 methid = ObjType->GetMethodIdByDecl(MethodName.c_str());
			if(methid < 0)
			{
				JELOG_E(jeStringc("Can't find method : ")+MethodName)
				return false;
			}

			return CallInternal(methid,AngelObject->Ptr);
		}

		//! Execute Call
		u1 CScriptManager_AngelScript::ExecuteCall()
		{
			s32 Result = ScriptContext->Execute();

			if(Result != asEXECUTION_FINISHED)
			{
				switch(Result)
				{
				case asEXECUTION_ABORTED:
					{
						JELOG_E("Script timeout")
						return false;
						break;
					}
				case asEXECUTION_EXCEPTION:
					{
						s32 ExpfuncID = ScriptContext->GetExceptionFunction();
						asIScriptFunction * Expfunc = Engine->GetFunctionDescriptorById(ExpfuncID);

						jeStringc ErrorMsg = "Script error : \n";
						ErrorMsg += jeStringc("Function : ") + jeStringc(Expfunc->GetDeclaration()) + jeStringc("\n");
						ErrorMsg += jeStringc("Module : ") + jeStringc(Expfunc->GetModuleName()) + jeStringc("\n");
						ErrorMsg += jeStringc("Section : ") + jeStringc(Expfunc->GetScriptSectionName()) + jeStringc("\n");
						ErrorMsg += jeStringc("Line : ") + jeStringc(ScriptContext->GetExceptionLineNumber()) + jeStringc("\n");
						ErrorMsg += jeStringc("Description : ") + jeStringc(ScriptContext->GetExceptionString()) + jeStringc("\n");

						JELOG_E(ErrorMsg)
						return false;
						break;
					}
				default:
					{
						JELOG_E(jeStringc("Script unknown error : ")+Result)
						return false;
					}
				}
			}

			return true;
		}

		//! Get Result
		s8 CScriptManager_AngelScript::GetResults8()
		{
			return ScriptContext->GetReturnByte();
		}

		//! Get Result
		s32 CScriptManager_AngelScript::GetResults32()
		{
			return ScriptContext->GetReturnDWord();
		}

		//! Get Result
		f32 CScriptManager_AngelScript::GetResultf32()
		{
			return ScriptContext->GetReturnFloat();
		}

		//! Get Result
		void * CScriptManager_AngelScript::GetResultPointer()
		{
			return ScriptContext->GetReturnAddress();
		}

		//! Create Script Object
		IScriptObject * CScriptManager_AngelScript::CreateScriptObject(const jeStringc & Name)
		{
			IScriptObject * Object = NULL;
			JENEW(Object,CScriptObject_AngelScript(Name,Engine))
			return Object;
		}

	}
}
